package flca.mda.api.util;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import mda.annotation.DatabaseUnit;
import mda.annotation.PersistenceType;
import mda.type.IEntityType;
import snippets.backend.PersistenceXmlDbUnit;

import flca.mda.codegen.helpers.StrUtil;

public class DbUnitUtils {

	private static TypeUtils tu = new TypeUtils();
	
	// a map of of DbUnit's. Key is the unit name a value (inner-class) DbUnit that contain all IEntityType that belong to this unit
	private static Map<String, DbUnit> dbUnitsMap = null;

	public DbUnitUtils() {
		fillDbUnitsMap();
	}

	public static void reset() {
		dbUnitsMap = null;
	}

	/**
	 * specific method to generate code in AppPersistenceXml.jet
	 * @return
	 */
	public String generatePersistenceXml() {
		return generateThePersistenceXml();
	}

	/**
	 * specific method to generate code in AppDbsProviders.jet
	 * @return
	 */
	public String generateRegisterDatabases() {
		return doGenerateRegisterDatabase();
	}
	
	/**
	 * specific method to generate code in AppDbsProviders.jet
	 * @return
	 */
	public String generateDbsProviders(String aAppContext) {
		return generateTheDbsProviders(aAppContext);
	}
	
	private static final String DEFAULT = "default"; //see FlcaConstants
	private static final String NONE = "none"; //see FlcaConstants
	
	/**
	 * return the database unit of the given class. 
	 * If none is found and it is an IEntityType then "default" will be returned else "none"
	 * @param aClass
	 * @return
	 */
	public String getDbUnitName(Class<?> aClass) {
		DatabaseUnit dbannot = (DatabaseUnit) tu.getAnnotation(aClass, DatabaseUnit.class);
		if (dbannot != null) {
			return dbannot.name();
		} else {
			if (tu.hasInterface(aClass, IEntityType.class)) {
				return DEFAULT;
			} else {
				return NONE;
			}
		}
	}

	/**
	 * return the database PersistenceType of the given class. 
	 * If none is found and it is an IEntityType then "JPA will be returned else NONE
	 * @param aClass
	 * @return
	 */
	public PersistenceType getDbUnitType(Class<?> aClass) {
		DatabaseUnit dbannot = (DatabaseUnit) tu.getAnnotation(aClass, DatabaseUnit.class);
		if (dbannot != null) {
			return dbannot.type();
		} else {
			if (tu.hasInterface(aClass, IEntityType.class)) {
				return PersistenceType.JPA;
			} else {
				return PersistenceType.NONE;
			}
		}
	}

	/**
	 * try to return the (best) PersistenceType used in the given method. It does so by looking
	 * for the first IEntityType in the arg list and uses that one. If no such arg is found NONE is returned
	 * TODO checg generic type
	 * @param aMethod
	 * @return
	 */
	public PersistenceType getDbUnitType(Method aMethod) {
		for (Class<?> clazz : aMethod.getParameterTypes()) {
			if (tu.hasAnnotation(clazz, IEntityType.class)) {
				return getDbUnitType(clazz);
			}
		}
		
		if (tu.hasAnnotation(aMethod.getReturnType(), IEntityType.class)) {
			return getDbUnitType(aMethod.getReturnType());
		} else {
			return PersistenceType.NONE;
		}
	}

	/**
	 * try to return the (best) dbs unit-name used in the given method. It does so by looking
	 * for the first IEntityType in the arg list and uses that one. If no such arg is found "none" is returned
	 * TODO check generic type
	 * @param aMethod
	 * @return
	 */
	public String getDbUnitName(Method aMethod) {
		for (Class<?> clazz : aMethod.getParameterTypes()) {
			if (tu.hasAnnotation(clazz, IEntityType.class)) {
				return getDbUnitName(clazz);
			}
		}
		
		if (tu.hasAnnotation(aMethod.getReturnType(), IEntityType.class)) {
			return getDbUnitName(aMethod.getReturnType());
		} else {
			return NONE;
		}
	}
	
	/**
	 * try to return the (best) class of type IEntityType used in the given method. It does so by looking
	 * for the first IEntityType in the arg list and uses that one. If no such arg is found null is returned
	 * TODO check generic type
	 * @param aMethod
	 * @return
	 */
	public Class<?> getEntityUsedInMethod(Method aMethod) {
		for (Class<?> clazz : aMethod.getParameterTypes()) {
			if (tu.hasAnnotation(clazz, IEntityType.class)) {
				return clazz;
			}
		}
		
		if (tu.hasAnnotation(aMethod.getReturnType(), IEntityType.class)) {
			return aMethod.getReturnType();
		} else {
			return null;
		}
	}

	private void fillDbUnitsMap() {
		if (dbUnitsMap == null || dbUnitsMap.isEmpty()) {
			dbUnitsMap = new HashMap<String , DbUnit>();
			
			List<Class<?>> entities = ModelClassesUtils.findModelClassesWithInterface(IEntityType.class);
			for (Class<?> entity : entities) {
				String name = getDbUnitName(entity);
				PersistenceType type = getDbUnitType(entity);
				if (dbUnitsMap.get(name) == null) {
					dbUnitsMap.put(name, new DbUnit(name, type, entity));
				} else {
					dbUnitsMap.get(name).addClass(entity);
				}
			}
		}
	}
	
	private String generateThePersistenceXml() {
		StringBuffer sb = new StringBuffer();
		for (DbUnit dbUnit : dbUnitsMap.values()) {
			if (PersistenceType.JPA.equals(dbUnit.type)) {
				sb.append(generateJpaUnit(dbUnit) + "\n");
			}
		}
		return sb.toString();
	}

	private static final String dbsRegister = "\t\t\tresult.add(new %DBS%(\"%UNITNAME%\"));";
	private static final String dbsProvider = "" +
	"	public static class DbUnit_%UNITNAME% implements Provider<EntityManager> { \n" + 
	"		public EntityManager get() {\n" +
	"			return (EntityManager) %APPCONTEXT%.getInstance().getConnPools().getDao(\"%UNITNAME%\").getObjectMgr();\n" +
	"		}\n" +
	"	}\n" ;
	   	 
	/**
	   	 List<RegisterDatabase> result = new ArrayList<RegisterDatabase>();
	   	 result.add(new com.flca.dbs.jpa.JpaDatabase("default"));
	   	 return result;
	 */
	private String doGenerateRegisterDatabase() {
		StringBuffer sb = new StringBuffer();
		sb.append("\t\t\tList<RegisterDatabase> result = new ArrayList<RegisterDatabase>(); \n");
		for (DbUnit dbUnit : dbUnitsMap.values()) {
			String s = StrUtil.replace(dbsRegister, "%UNITNAME%", dbUnit.name) + "\n";
			s = StrUtil.replace(s, "%DBS%", regDbsMap.get(dbUnit.type));
			sb.append(s + "\n");
		}
		sb.append("\t\t\treturn result;");
		return sb.toString();
	}


	/**
	 * this will return something like:
		public static class DbUnit_default implements Provider<EntityManager> {
		public EntityManager get() {
			return (EntityManager) DemoContext.getInstance().getConnPools().getDao(DEFAULT).getObjectMgr();
		}
	}
	 * @return
	 */
	private String generateTheDbsProviders(String aAppContext) {
		StringBuffer sb = new StringBuffer();
		for (DbUnit dbUnit : dbUnitsMap.values()) {
			String s = StrUtil.replace(dbsProvider, "%UNITNAME%", dbUnit.name);
			s = StrUtil.replace(s, "%APPCONTEXT%", aAppContext);
			sb.append(s + "\n");
		}
		return sb.toString();
	}
			
	private String generateJpaUnit(DbUnit aDbUnit) {
		return tu.include(PersistenceXmlDbUnit.class, new Object[] {aDbUnit.name, aDbUnit.classes});
	}
	
//	private static final String Q = "\"";
	
	@SuppressWarnings("serial")
	private static Map<PersistenceType, String> regDbsMap = new HashMap<PersistenceType, String>() {{
		put(PersistenceType.JPA, "com.flca.dbs.jpa.JpaDatabase");
		put(PersistenceType.PERST, "com.flca.dbs.perst.PerstDatabase");
//		put(PersistenceType.CACHE, "com.flca.dbs.cache.CacheDatabase");
		put(PersistenceType.BERKELEY, "com.flca.dbs.berkeley.BerkeleyDatabase");
		put(PersistenceType.APPENGINE, "com.flca.dbs.appengine.AppEngineDatabase");
		put(PersistenceType.NONE, "com.flca.frw.dbs.DummyDatabase");
	}};
	
	//---- inner class ----
	private class DbUnit {
		PersistenceType type;
		String name;
		Set<Class<?>> classes;
		
		public DbUnit(String name, PersistenceType type, Class<?> aClass) {
			super();
			this.type = type;
			this.name = name;
			classes = new HashSet<Class<?>>();
			classes.add(aClass);
		}
		
		public void addClass(Class<?> aClass) {
			classes.add(aClass);
		}
	}
}
